/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package pt.utl.ist.isr.livetrack.server.impl;

import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import pt.utl.ist.isr.livetrack.server.interfaces.Camera;
import pt.utl.ist.isr.livetrack.server.interfaces.Blob;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.log4j.Logger;
import javax.imageio.ImageIO;
import pt.utl.ist.isr.livetrack.analysis.ImageUtils;
import pt.utl.ist.isr.livetrack.server.interfaces.RoiType;

/**
 *
 * @author David Miguel Antunes <davidmiguel [ at ] antunes.net>
 */
public abstract class DefaultCamera implements Camera {

    public static Logger logger = Logger.getLogger(DefaultCamera.class.getName());
    protected boolean[][] kernel = new boolean[][]{
        {true, true, true},
        {true, true, true},
        {true, true, true},};
    protected String id;
    protected String zoneId;
    protected ImageUtils iu;
    protected int[] nextBackground;
    protected int[] img;
    protected int[] subtracted;
    protected int[] marked;
    protected int[] background;
    protected boolean[] binary;
    protected boolean[] dilatedOut;
    protected byte[] imgByteArray;
    protected byte[] subtractedByteArray;
    protected byte[] binaryByteArray;
    protected byte[] backgroundByteArray;
    protected ArrayList<Blob> blobs;
    protected int WIDTH;
    protected int HEIGHT;
    protected int NCOMP;
    protected int binaryThreshold = 127;
    protected int kernelHeight = 3;
    protected int kernelWidth = 3;
    protected boolean applyMorphologicalClosing = true;
    protected int minBlobArea = 0;
    protected int maxBlobArea = Integer.MAX_VALUE;
    protected int minBlobActivePixels = 0;
    protected int maxBlobActivePixels = Integer.MAX_VALUE;
    protected boolean optimizeClosing = true;
    protected Rectangle2D roi = new Rectangle();
    protected RoiType roiType = RoiType.DISABLED;

    public DefaultCamera() {
    }

    public DefaultCamera(String id) {
        this.id = id;
        iu = new ImageUtils(0, 0, 0);
    }

    private byte[] imageToByteArray(BufferedImage bi, String format) {
        byte[] array;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.setUseCache(false);
            ImageIO.write(bi, format, baos);
            array = baos.toByteArray();
        } catch (IOException ex) {
            logger.error(ex);
            array = null;
        }
        return array;
    }

    protected synchronized void processNewImage() {
        if (nextBackground == null) {
            nextBackground = iu.deepCopy(img);
        }
        if (background == null) {
            background = new int[img.length];
        }
        if (subtracted == null) {
            subtracted = iu.newImage();
        }
        if (binary == null) {
            binary = iu.newBinaryImage();
        }
        if (dilatedOut == null) {
            dilatedOut = iu.newBinaryImage();
        }
        if (marked == null) {
            marked = iu.newIntensityImage();
        }

        System.arraycopy(nextBackground, 0, background, 0, nextBackground.length);

        reprocessImage();
    }

    protected synchronized void reprocessImage() {

        long start, all = System.currentTimeMillis(), backgroundT, binaryT, closingT, compExtractT, compToBlobsT;

        imgByteArray = binaryByteArray = subtractedByteArray = backgroundByteArray = null;

        start = System.currentTimeMillis();
        calculateNextBackground();
        backgroundT = System.currentTimeMillis() - start;

        start = System.currentTimeMillis();
        calculateSubtractedAndBinary();
        binaryT = System.currentTimeMillis() - start;

        start = System.currentTimeMillis();
        if (applyMorphologicalClosing) {
            boolean optDilate = false, optErode = false;
            if (optimizeClosing) {
                int t = 0;
                for (int i = 0; i < binary.length; i++) {
                    if (binary[i]) {
                        t++;
                    }
                }
                if (t > binary.length) {
                    optErode = true;
                } else {
                    optDilate = true;
                }
            }
            iu.dilate(binary, dilatedOut, kernel, optDilate);
            iu.erode(dilatedOut, binary, kernel, optErode);
        }
        closingT = System.currentTimeMillis() - start;

        start = System.currentTimeMillis();
        int nConnectedComponents = iu.extractConnectedComponents(binary, marked);
        compExtractT = System.currentTimeMillis() - start;


        start = System.currentTimeMillis();
        blobs = iu.connectedComponentsToBlobs(marked, nConnectedComponents, id);
        compToBlobsT = System.currentTimeMillis() - start;

        Iterator<Blob> it = blobs.iterator();
        while (it.hasNext()) {
            Blob blob = it.next();
            boolean remove = false;

            // Check area and active pixels:
            if (blob.getArea() < minBlobArea
                    || blob.getArea() > maxBlobArea
                    || blob.getSize() < minBlobActivePixels
                    || blob.getSize() > maxBlobActivePixels) {
                remove = true;
            }

            // Check ROI:
            if (!remove) {
                boolean intersectsRoi = getRoi().intersects(
                        blob.getTopLeftX(),
                        blob.getTopLeftY(),
                        blob.getBottomRightX() - blob.getTopLeftX(),
                        blob.getBottomRightY() - blob.getTopLeftY());
                switch (roiType) {
                    case ACCEPT:
                        if (!intersectsRoi) {
                            remove = true;
                        }
                        break;
                    case REJECT:
                        if (intersectsRoi) {
                            remove = true;
                        }
                        break;
                }
            }
            if (remove) {
                it.remove();
            }
        }

        logger.debug("Image processing took " + (System.currentTimeMillis() - all) + " millis");
        logger.debug("Details:\nbackgroundT=" + backgroundT + "\nbinaryT=" + binaryT + "\nclosingT=" + closingT + "\ncompExtractT=" + compExtractT + "\ncompToBlobsT=" + compToBlobsT + "");
    }

    private void calculateNextBackground() {
        for (int l = 0; l < HEIGHT; l++) {
            for (int c = 0; c < WIDTH; c++) {

                final int offset = l * WIDTH * NCOMP + c * NCOMP;

                if (img[offset] > nextBackground[offset]) {
                    nextBackground[offset]++;
                } else if (img[offset] < nextBackground[offset]) {
                    nextBackground[offset]--;
                }
                if (img[offset + 1] > nextBackground[offset + 1]) {
                    nextBackground[offset + 1]++;
                } else if (img[offset + 1] < nextBackground[offset + 1]) {
                    nextBackground[offset + 1]--;
                }
                if (img[offset + 2] > nextBackground[offset + 2]) {
                    nextBackground[offset + 2]++;
                } else if (img[offset + 2] < nextBackground[offset + 2]) {
                    nextBackground[offset + 2]--;
                }
            }
        }
    }

    private void calculateSubtractedAndBinary() {
        for (int l = 0; l < HEIGHT; l++) {
            for (int c = 0; c < WIDTH; c++) {

                final int offset = l * WIDTH * NCOMP + c * NCOMP;

                subtracted[offset + 0] =
                        img[offset + 0] > background[offset + 0]
                        ? img[offset + 0] - background[offset + 0]
                        : background[offset + 0] - img[offset + 0];

                subtracted[offset + 1] =
                        img[offset + 1] > background[offset + 1]
                        ? img[offset + 1] - background[offset + 1]
                        : background[offset + 1] - img[offset + 1];

                subtracted[offset + 2] =
                        img[offset + 2] > background[offset + 2]
                        ? img[offset + 2] - background[offset + 2]
                        : background[offset + 2] - img[offset + 2];

                int inten = subtracted[offset + 0] + subtracted[offset + 1] + subtracted[offset + 2];
                inten /= 3;
                if (inten > binaryThreshold) {
                    binary[l * WIDTH + c] = true;
                } else {
                    binary[l * WIDTH + c] = false;
                }
            }
        }
    }

    @Override
    public int getBinaryThreshold() {
        return binaryThreshold;
    }

    @Override
    public void setBinaryThreshold(int binaryThreshold) {
        this.binaryThreshold = binaryThreshold;
        reprocessImage();
    }

    @Override
    public boolean isApplyMorphologicalClosing() {
        return applyMorphologicalClosing;
    }

    @Override
    public void setApplyMorphologicalClosing(boolean applyMorphologicalClosing) {
        this.applyMorphologicalClosing = applyMorphologicalClosing;
        reprocessImage();
    }

    @Override
    public int getKernelHeight() {
        return kernelHeight;
    }

    @Override
    public void setKernelHeight(int kernelHeight) {
        if (kernelHeight > 0 && kernelHeight % 2 == 1) {
            this.kernelHeight = kernelHeight;
            kernel = new boolean[kernelHeight][kernelWidth];
            for (int i = 0; i < kernel.length; i++) {
                for (int j = 0; j < kernel[0].length; j++) {
                    kernel[i][j] = true;
                }
            }
            reprocessImage();
        }
    }

    @Override
    public int getKernelWidth() {
        return kernelWidth;
    }

    @Override
    public void setKernelWidth(int kernelWidth) {
        if (kernelWidth > 0 && kernelWidth % 2 == 1) {
            this.kernelWidth = kernelWidth;
            kernel = new boolean[kernelHeight][kernelWidth];
            for (int i = 0; i < kernel.length; i++) {
                for (int j = 0; j < kernel[0].length; j++) {
                    kernel[i][j] = true;
                }
            }
            reprocessImage();
        }
    }

    @Override
    public int getWidth() {
        return WIDTH;
    }

    @Override
    public int getHeight() {
        return HEIGHT;
    }

    @Override
    public int getNComp() {
        return NCOMP;
    }

    @Override
    public synchronized BufferedImage getImage() {
        if (img != null) {
            BufferedImage bi = iu.arrayToBufferedImage(img);
            postprocessImage(bi);
            return bi;
        } else {
            return null;
        }
    }

    @Override
    public byte[] getImageAsByteArray() {
        return img == null ? null
                : imgByteArray == null ? (imgByteArray = imageToByteArray(getImage(), "jpg")) : imgByteArray;
    }

    @Override
    public int[] getImageAsIntArray() {
        return img;
    }

    @Override
    public synchronized BufferedImage getBackground() {
        if (background != null) {
            return iu.arrayToBufferedImage(background);
        } else {
            return null;
        }
    }

    @Override
    public byte[] getBackgroundAsByteArray() {
        return background == null ? null
                : backgroundByteArray == null ? (backgroundByteArray = imageToByteArray(getBackground(), "jpg")) : backgroundByteArray;
    }

    @Override
    public int[] getBackgroundAsIntArray() {
        return img;
    }

    @Override
    public synchronized BufferedImage getSubtracted() {
        if (subtracted != null) {
            return iu.arrayToBufferedImage(subtracted);
        } else {
            return null;
        }
    }

    @Override
    public byte[] getSubtractedAsByteArray() {
        return subtracted == null ? null
                : subtractedByteArray == null ? (subtractedByteArray = imageToByteArray(getSubtracted(), "jpg")) : subtractedByteArray;
    }

    @Override
    public int[] getSubtractedAsIntArray() {
        return img;
    }

    @Override
    public synchronized BufferedImage getBinary() {
        if (binary != null) {
            return iu.arrayToBufferedImage(binary);
        } else {
            return null;
        }
    }

    /**
     * @return Image format is GIF
     */
    @Override
    public byte[] getBinaryAsByteArray() {
        return binary == null ? null
                : binaryByteArray == null ? (binaryByteArray = imageToByteArray(getBinary(), "gif")) : binaryByteArray;
    }

    @Override
    public int[] getBinaryAsIntArray() {
        return img;
    }

    @Override
    public synchronized ArrayList<Blob> getBlobs() {
        return blobs;
    }

    @Override
    public int getMaxBlobActivePixels() {
        return maxBlobActivePixels;
    }

    @Override
    public void setMaxBlobActivePixels(int maxBlobActivePixels) {
        this.maxBlobActivePixels = maxBlobActivePixels;
        reprocessImage();
    }

    @Override
    public int getMaxBlobArea() {
        return maxBlobArea;
    }

    @Override
    public void setMaxBlobArea(int maxBlobArea) {
        this.maxBlobArea = maxBlobArea;
        reprocessImage();
    }

    @Override
    public int getMinBlobActivePixels() {
        return minBlobActivePixels;
    }

    @Override
    public void setMinBlobActivePixels(int minBlobActivePixels) {
        this.minBlobActivePixels = minBlobActivePixels;
        reprocessImage();
    }

    @Override
    public int getMinBlobArea() {
        return minBlobArea;
    }

    @Override
    public void setMinBlobArea(int minBlobArea) {
        this.minBlobArea = minBlobArea;
        reprocessImage();
    }

    @Override
    public boolean isOptimizeClosing() {
        return optimizeClosing;
    }

    @Override
    public void setOptimizeClosing(boolean optimizeClosing) {
        this.optimizeClosing = optimizeClosing;
        reprocessImage();
    }

    @Override
    public Rectangle2D getRoi() {
        return roi;
    }

    @Override
    public void setRoi(Rectangle2D roi) {
        this.roi = roi;
        reprocessImage();
    }

    @Override
    public RoiType getRoiType() {
        return roiType;
    }

    @Override
    public void setRoiType(RoiType roiType) {
        this.roiType = roiType;
        reprocessImage();
    }

    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getZoneId() {
        return zoneId;
    }

    @Override
    public void setZoneId(String zoneId) {
        this.zoneId = zoneId;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final DefaultCamera other = (DefaultCamera) obj;
        if ((this.id == null) ? (other.id != null) : !this.id.equals(other.id)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 53 * hash + (this.id != null ? this.id.hashCode() : 0);
        return hash;
    }

    // Protected methods:
    protected abstract void postprocessImage(BufferedImage bi);
}
